package next.fire.spinus.logx.provider.huawei.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import next.fire.spinus.logx.api.LogService;
import next.fire.spinus.logx.api.model.*;
import next.fire.spinus.logx.api.query.DateRange;
import next.fire.spinus.logx.api.query.LogDataQuery;
import next.fire.spinus.logx.base.AbstractClient;
import next.fire.spinus.logx.base.RequestHandler;
import next.fire.spinus.logx.provider.huawei.client.HuaweiLogClient;
import next.fire.spinus.logx.provider.huawei.client.HuaweiLogPutClient;
import next.fire.spinus.logx.provider.huawei.client.HuaweiLogTokenClient;
import next.fire.spinus.logx.provider.huawei.conf.HuaweiLogConfig;
import next.fire.spinus.logx.provider.huawei.conf.HuaweiLogConfigHolder;
import next.fire.spinus.logx.provider.huawei.conf.HuaweiLogToken;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Created by daibing on 2023/4/13.
 */
public class HuaweiLogServiceImpl implements LogService {
    private final HuaweiLogClient logClient = new HuaweiLogClient();
    private final HuaweiLogTokenClient logTokenClient = new HuaweiLogTokenClient();
    private final HuaweiLogPutClient logPutClient = new HuaweiLogPutClient();
    private final ConcurrentMap<String, HuaweiLogToken> logStoreCode2Token = new ConcurrentHashMap<>();

    @Override
    public ProviderEnum getProvider() {
        return ProviderEnum.HUAWEI;
    }

    @SafeVarargs
    @Override
    public final void loadConf(Map<String, Object>... params) {
        List<HuaweiLogConfig> huaweiLogConfigList = new ArrayList<>();
        for (Map<String, Object> param : params) {
            JSONObject conf = (JSONObject) param;
            if (!ProviderEnum.HUAWEI.name().equalsIgnoreCase(conf.getString("provider"))) {
                continue;
            }
            huaweiLogConfigList.add(conf.toJavaObject(HuaweiLogConfig.class));
        }
        if (huaweiLogConfigList.isEmpty()) {
            return;
        }
        HuaweiLogConfigHolder.get().reload(huaweiLogConfigList);
    }

    /**
     * 查询帐号下所有日志组
     * https://support.huaweicloud.com/api-lts/lts_api_0013.html
     */
    @Override
    public LogMeta getMeta(String logStoreCode) {
        HuaweiLogConfig config = HuaweiLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("huawei log config not found by " + logStoreCode);
        }
        String resourceUri = String.format("/v2/%s/groups", config.getProjectId());
        return logClient.execute(AbstractClient.Method.GET, resourceUri, logStoreCode, false, false, new RequestHandler<LogMeta>() {
            @Override
            public Map<String, Object> buildBizParams() {
                return null;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                return null;
            }

            @Override
            public LogMeta extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                JSONArray logGroups = data.getJSONArray("log_groups");
                for (int i = 0; i < logGroups.size(); i++) {
                    JSONObject jsonObject = logGroups.getJSONObject(i);
                    if (!jsonObject.getString("log_group_id").equalsIgnoreCase(config.getLogGroupId())) {
                        continue;
                    }
                    LogMeta meta = new LogMeta();
                    meta.setLogStoreName(jsonObject.getString("log_group_name"));
                    meta.setTtl(jsonObject.getInteger("ttl_in_days"));
                    meta.setShardCount(jsonObject.getInteger("stream_size"));
                    meta.setAutoSplit(false);
                    meta.setCreateTime(parseTimestamp(jsonObject.getLong("creation_time")));
                    meta.setLastModify(null);
                    return meta;
                }
                return null;
            }
        });
    }

    /**
     * 查询结构化配置
     * https://support.huaweicloud.com/api-lts/lts_api_0062.html
     */
    @Override
    public List<LogField> listField(String logStoreCode) {
        HuaweiLogConfig config = HuaweiLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("huawei log config not found by " + logStoreCode);
        }
        String resourceUri = String.format("/v2/%s/lts/struct/template?logGroupId=%s&logStreamId=%s",
                config.getProjectId(), config.getLogGroupId(), config.getLogStreamId());
        return logClient.execute(AbstractClient.Method.GET, resourceUri, logStoreCode, false, false, new RequestHandler<List<LogField>>() {
            @Override
            public Map<String, Object> buildBizParams() {
                return null;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                JSONObject accessParams = new JSONObject(1);
                accessParams.put(HuaweiLogClient.UNESCAPE_JSON, true);
                return accessParams;
            }

            @Override
            public List<LogField> extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                List<LogField> fields = new ArrayList<>();
                JSONArray fieldArray = data.getJSONArray("demoFields");
                for (int i = 0; i < fieldArray.size(); i++) {
                    JSONObject fieldObj = fieldArray.getJSONObject(i);
                    LogField field = new LogField();
                    field.setFieldName(fieldObj.getString("fieldName"));
                    field.setIndexMode(String.valueOf(fieldObj.getInteger("index")));
                    field.setDocValue(fieldObj.getBooleanValue("isAnalysis"));
                    field.setText("string".equalsIgnoreCase(fieldObj.getString("type")));
                    fields.add(field);
                }
                return fields;
            }
        });
    }

    /**
     * Token认证
     * https://support.huaweicloud.com/api-lts/lts_api_0005.html
     */
    public HuaweiLogToken getToken(String logStoreCode) {
        HuaweiLogConfig config = HuaweiLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("huawei log config not found by " + logStoreCode);
        }
        String resourceUri = "/v3/auth/tokens";
        String body = String.format("{\n" +
                "  \"auth\": {\n" +
                "    \"identity\": {\n" +
                "      \"methods\": [\n" +
                "        \"password\"\n" +
                "      ],\n" +
                "      \"password\": {\n" +
                "        \"user\": {\n" +
                "          \"domain\": {\n" +
                "            \"name\": \"%s\"\n" +
                "          },\n" +
                "          \"name\": \"%s\",\n" +
                "          \"password\": \"%s\"\n" +
                "        }\n" +
                "      }\n" +
                "    },\n" +
                "    \"scope\": {\n" +
                "      \"project\": {\n" +
                "        \"id\": \"%s\"\n" +
                "      }\n" +
                "    }\n" +
                "  }\n" +
                "}", config.getAccountName(), config.getIamUserName(), config.getIamPassword(), config.getProjectId());
        return logTokenClient.execute(AbstractClient.Method.POST, resourceUri, logStoreCode, false, false, new RequestHandler<HuaweiLogToken>() {
            @Override
            public Map<String, Object> buildBizParams() {
                return JSON.parseObject(body);
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                return null;
            }

            @Override
            public HuaweiLogToken extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                HuaweiLogToken token = new HuaweiLogToken();
                token.setLogStoreCode(config.getLogStoreCode());
                token.setProjectId(config.getProjectId());
                token.setToken(data.getString(HuaweiLogTokenClient.LOG_TOKEN));
                token.setIssuedTime(logClient.parseISO8601(data.getJSONObject("token").getString("issued_at")).getTime());
                token.setExpiresTime(logClient.parseISO8601(data.getJSONObject("token").getString("expires_at")).getTime());
                return token;
            }
        });
    }

    /**
     * 上报高精度日志
     * https://support.huaweicloud.com/usermanual-lts/lts_04_0217.html
     */
    @Override
    public String putData(String logStoreCode, List<LogData> dataList) {
        if (dataList == null || dataList.isEmpty()) {
            return null;
        }
        HuaweiLogConfig config = HuaweiLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("huawei log config not found by " + logStoreCode);
        }
        String resourceUri = String.format("/v2/%s/lts/groups/%s/streams/%s/tenant/contents/high-accuracy",
                config.getProjectId(), config.getLogGroupId(), config.getLogStreamId());
        return logPutClient.execute(AbstractClient.Method.POST, resourceUri, logStoreCode, false, false, new RequestHandler<String>() {
            @Override
            public Map<String, Object> buildBizParams() {
                JSONArray logItems = new JSONArray(dataList.size());
                for (LogData logData : dataList) {
                    JSONObject logItem = new JSONObject(2);
                    logItem.put("log_time_ns", logData.getLogTime());
                    logItem.put("log", JSON.toJSONString(logData));
                    logItems.add(logItem);
                }
                JSONObject data = new JSONObject(2);
                data.put("contents", logItems);
                data.put("labels", new JSONObject(0));
                return data;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                HuaweiLogToken logToken = getTokenByCacheOrAuthTokens(logStoreCode);
                if (logToken == null || isBlank(logToken.getToken())) {
                    throw new RuntimeException("HuaweiLog put data failed by invalid token : " + logStoreCode);
                }
                JSONObject accessParams = new JSONObject(1);
                accessParams.put(HuaweiLogPutClient.AUTH_TOKEN, logToken.getToken());
                return accessParams;
            }

            @Override
            public String extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                return data.getString(HuaweiLogPutClient.LOG_PUT_REQUEST_ID);
            }
        });
    }

    @Override
    public List<LogData> listData(String logStoreCode, LogDataQuery query, int pageNo, int pageSize) {
        throw new UnsupportedOperationException("Huawei log query should by listDataByOffset!");
    }

    /**
     * 查询日志：offsetPosition对应line_num，日志单行序列号，第一次查询时不需要此参数，后续分页查询时需要使用，可从上次查询的返回信息中获取。
     * https://support.huaweicloud.com/api-lts/lts_api_0028.html
     */
    @Override
    public OffsetResult<LogData> listDataByOffset(String logStoreCode, LogDataQuery query, String offsetPosition, int offsetSize) {
        if (query == null || query.getDateRange() == null || query.getDateRange().getStartTime() == null || query.getDateRange().getEndTime() == null) {
            throw new IllegalArgumentException("query log must set date range: " + logStoreCode);
        }
        HuaweiLogConfig config = HuaweiLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("huawei log config not found by " + logStoreCode);
        }
        String resourceUri = String.format("/v2/%s/groups/%s/streams/%s/content/query",
                config.getProjectId(), config.getLogGroupId(), config.getLogStreamId());
        return logClient.execute(AbstractClient.Method.POST, resourceUri, logStoreCode, false, false,
                new RequestHandler<OffsetResult<LogData>>() {
            @Override
            public Map<String, Object> buildBizParams() {
                JSONObject params = new JSONObject();
                // 时间区间字段必填，这里是日志采集时间，不是日志打印时间，精度为毫秒，所以将结束时间调整为查询日志结束时间+3分钟
                params.put("start_time", query.getDateRange().getStartTime().getTime());
                params.put("end_time", query.getDateRange().getEndTime().getTime() + 3 * 60 * 1000);
                params.put("keywords", buildQuery(query));
                if (!isBlank(offsetPosition)) {
                    params.put("line_num", offsetPosition);
                }
                params.put("is_desc", query.isReverse());
                params.put("search_type", getSearchType(offsetPosition));
                params.put("limit", offsetSize);
                params.put("highlight", false);
                params.put("is_count", true);
                return params;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                return null;
            }

            @Override
            public OffsetResult<LogData> extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                String position = offsetPosition;
                int size = data.getInteger("count");
                JSONArray logs = data.getJSONArray("logs");
                if (logs == null) {
                    return new OffsetResult<>(position, size, Collections.emptyList());
                }

                List<LogData> result = new ArrayList<>(logs.size());
                for (int i = 0; i < logs.size(); i++) {
                    JSONObject log = logs.getJSONObject(i);
                    // labels返回数据取决于索引结构定义，索引结构不一定完整，所以从content获取日志内容。
                    result.add(JSON.parseObject(log.getString("content"), LogData.class));
                    if (i == logs.size() - 1) {
                        position = log.getString("line_num");
                    }
                }
                return new OffsetResult<>(position, size, result);
            }
        });
    }

    /**
     * 查询结构化日志（新版）
     * https://support.huaweicloud.com/api-lts/lts_api_0050.html
     */
    public List<LogData> listDataByStructQuery(String logStoreCode, LogDataQuery query) {
        if (query == null || query.getDateRange() == null || query.getDateRange().getStartTime() == null || query.getDateRange().getEndTime() == null) {
            throw new RuntimeException("struct query log must set date range: " + logStoreCode);
        }
        if (isBlank(query.getQueryExpression())) {
            throw new RuntimeException("struct query log must set query expression: " + logStoreCode);
        }
        HuaweiLogConfig config = HuaweiLogConfigHolder.get().select(logStoreCode);
        if (config == null) {
            throw new RuntimeException("huawei log config not found by " + logStoreCode);
        }
        String resourceUri = String.format("/v2/%s/streams/%s/struct-content/query", config.getProjectId(), config.getLogStreamId());
        return logClient.execute(AbstractClient.Method.POST, resourceUri, logStoreCode, false, false, new RequestHandler<List<LogData>>() {
            @Override
            public Map<String, Object> buildBizParams() {
                JSONObject params = new JSONObject(4);
                params.put("query", query.getQueryExpression());
                params.put("format", "k-v");
                params.put("time_range", buildTimeRangeObj(query.getDateRange()));
                params.put("whether_to_rows", true);
                return params;
            }

            @Override
            public Map<String, Object> buildAccessParams() {
                return null;
            }

            @Override
            public List<LogData> extractData(Map<String, Object> output) {
                JSONObject data = (JSONObject) output;
                return data.getJSONArray("result").toJavaList(LogData.class);
            }
        });
    }

    @Override
    public List<String> listAppId(String logStoreCode, String stage) {
        LogDataQuery query = new LogDataQuery();
        query.setStage(stage);
        query.setDateRange(new DateRange(new Date(), 24 * 60, true));
        query.setQueryExpression("select distinct(appId) limit 600");
        List<LogData> logDataList = listDataByStructQuery(logStoreCode, query);
        List<String> appIds = new ArrayList<>(logDataList.size());
        for (LogData logData : logDataList) {
            appIds.add(logData.getAppId());
        }
        return appIds;
    }

    private HuaweiLogToken getTokenByCacheOrAuthTokens(String logStoreCode) {
        long aheadMillis = 10 * 60 * 1000L;
        HuaweiLogToken huaweiLogToken = logStoreCode2Token.get(logStoreCode);
        if (huaweiLogToken != null && huaweiLogToken.getExpiresTime() - aheadMillis < System.currentTimeMillis()) {
            return huaweiLogToken;
        }
        synchronized (logStoreCode2Token) {
            huaweiLogToken = logStoreCode2Token.get(logStoreCode);
            if (huaweiLogToken != null && huaweiLogToken.getExpiresTime() - aheadMillis < System.currentTimeMillis()) {
                return huaweiLogToken;
            }
            huaweiLogToken = this.getToken(logStoreCode);
            logStoreCode2Token.put(logStoreCode, huaweiLogToken);
        }
        return huaweiLogToken;
    }

    /**
     * 首次查询为 “init”, 分页查询时为 “forwards”或者“backwards”, 默认为首次查询“init”, 与“is_desc”参数配合进行分页查询。
     */
    private String getSearchType(String offsetPosition) {
        if (isBlank(offsetPosition)) {
            return "init";
        }
        return "forwards";
    }

    /**
     * 时间区间字段必填，这里是日志采集时间，不是日志打印时间，精度为毫秒，所以将结束时间调整为查询日志结束时间+3分钟
     */
    private JSONObject buildTimeRangeObj(DateRange dateRange) {
        JSONObject timeRangeParams = new JSONObject(5);
        timeRangeParams.put("sql_time_zone", "UTC");
        timeRangeParams.put("start_time", dateRange.getStartTime().getTime());
        timeRangeParams.put("end_time", dateRange.getEndTime().getTime() + 3 * 60 * 1000);
        timeRangeParams.put("start_time_gt", true);
        timeRangeParams.put("end_time_lt", false);
        return timeRangeParams;
    }

    /**
     * sql语句字符串
     * https://support.huaweicloud.com/usermanual-lts/lts_05_0005.html
     */
    private String buildQuery(LogDataQuery query) {
        String operator = " and ";
        StringBuilder sb = new StringBuilder();
        if (!this.isBlank(query.getAppId())) {
            sb.append("appId:\"").append(query.getAppId()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getStage())) {
            sb.append("stage:\"").append(query.getStage()).append("\"").append(operator);
        }
        // 日志级别需要向上回溯，比如选择INFO级别可以看到INFO和INFO以上的日志，包括WARN, ERROR
        if (query.getLevel() != null && query.getLevel() != LevelEnum.ALL) {
            String levelQuery = this.buildLevelQuery(query.getLevel());
            sb.append(" (").append(levelQuery).append(")").append(operator);
        }
        // 时间区间遵循“左闭右开”原则
        if (query.getDateRange() != null) {
            sb.append("logTime in [")
                    .append(query.getDateRange().getStartTime().getTime()).append(" ")
                    .append(query.getDateRange().getEndTime().getTime())
                    .append(")").append(operator);
        }
        if (!this.isBlank(query.getTraceId())) {
            sb.append("traceId:\"").append(query.getTraceId()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getFrontendId())) {
            sb.append("frontendId:\"").append(query.getFrontendId()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getHost())) {
            sb.append("host:\"").append(query.getHost()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getThread())) {
            sb.append("thread:\"").append(query.getThread()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getLocation())) {
            sb.append("location:\"").append(query.getLocation()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getMessage())) {
            sb.append("message:\"").append(query.getMessage()).append("\"").append(operator);
        }
        if (!this.isBlank(query.getThrowable())) {
            sb.append("throwable:\"").append(query.getThrowable()).append("\"").append(operator);
        }
        // 前端自己定义查询表达式
        if (!this.isBlank(query.getQueryExpression())) {
            sb.append(query.getQueryExpression()).append(operator);
        }
        return sb.substring(0, sb.length() - operator.length());
    }

    private String buildLevelQuery(LevelEnum level) {
        if (LevelEnum.ALL == level) {
            return null;
        }
        boolean matched = false;
        String operator = " or ";
        StringBuilder sb = new StringBuilder();
        for (LevelEnum value : LevelEnum.values()) {
            if (value == level) {
                matched = true;
            }
            if (matched) {
                sb.append("level:\"").append(value).append("\"").append(operator);
            }
        }
        return sb.substring(0, sb.length() - operator.length());
    }

    private Date parseTimestamp(Long timestamp) {
        if (timestamp.toString().length() == 13) {
            return new Date(timestamp);
        }
        return new Date(timestamp.intValue() * 1000L);
    }

    protected boolean isBlank(String s) {
        return s == null || s.trim().length() == 0;
    }
}
